﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

using OpenTK.Audio;
using OpenTK.Audio.OpenAL;

namespace com.ReinforceLab.AudioModem
{
    /// <summary>
    /// オーディオ物理インタフェース。
    /// 録音バッファ    
    ///   周期的に読み出すこと。放置すればバッファオーバフロー。使わないときはStop()。
    ///   読み出しバッファは、Read()して直ちに使用すること。バッファは内部で再利用。Read()の都度値が変わる。
    /// </summary>
    public class OpenALSocket :  IAudioPHY
    {
        #region Variables
        const int _threadPeriodMilliSec = 20;
        const int _maxSendQueueLength = 3;
        const double _bufferMargine = 1.1;

        readonly int     _samplingRate;
        readonly short[] _readBuffer;
        readonly int _minimumBufferLength;
        
        private volatile bool _workerShouldRun;

        AudioContext _audioContext;
        AudioCapture _audioCapture;
        IAudioSource _source;
        int _audioSource;
        
        Thread _thread;        
        #endregion

        #region Constructor
        public OpenALSocket(int samplingRate)
        {
            if (samplingRate <= 0 || samplingRate > 44100)
                throw new ArgumentOutOfRangeException("samplingRate");

            _samplingRate = samplingRate;            
            _minimumBufferLength = (int)(_samplingRate * _threadPeriodMilliSec * _bufferMargine / 1000.0);
            _readBuffer = new short[_minimumBufferLength * 2];
        }
        #endregion

        #region Private methods
        void clearBuffer(int buff)
        {
            int[] buffers;
            if (buff == 0)
            {
                int buffer_processed;
                AL.GetSource(_audioSource, ALGetSourcei.BuffersProcessed, out buffer_processed);
                if (buffer_processed == 0)
                    return;
                buffers = AL.SourceUnqueueBuffers(_audioSource, buffer_processed);
            }
            else
            {
                buffers = AL.SourceUnqueueBuffers(_audioSource, buff);
            }
            AL.DeleteBuffers(buffers);
        }
        void worker(Object obj)
        {
            System.Diagnostics.Debug.Write("Staring modem thread.\n");
            while (_workerShouldRun)
            {
                // send-queue unqueue 
                clearBuffer(0);
                
                // check sending queue
                int send_queue_len = 0;
                AL.GetSource(_audioSource, ALGetSourcei.BuffersQueued, out send_queue_len);
                
                // send data
                if (send_queue_len < _maxSendQueueLength)
                {
                    int validLength;
                    var buffer = _source.OutSignal(out validLength);

                    int buf = AL.GenBuffer();
                    //System.Diagnostics.Debug.WriteLine("OpenAL: write buffer.");
                    AL.BufferData(buf, ALFormat.Mono16, buffer, buffer.Length * OpenTK.BlittableValueType.StrideOf(buffer), _samplingRate);
                    AL.SourceQueueBuffer(_audioSource, buf);

                    if (AL.GetSourceState(_audioSource) != ALSourceState.Playing)
                        AL.SourcePlay(_audioSource);
                }

                // receive data
                int available_samples = _audioCapture.AvailableSamples;
                if (available_samples > 0)
                {
                    int buf_len = Math.Min(_readBuffer.Length, available_samples);
                    _audioCapture.ReadSamples(_readBuffer, buf_len);
                    _source.InSignal(_readBuffer, buf_len);
                }

                Thread.Sleep(_threadPeriodMilliSec - 5); // FIXME
            }
            System.Diagnostics.Debug.Write("Finished modem thread.\n");
        }
        #endregion

        #region INotifyPropertyChanged メンバ
        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        void invokePropertyChanged(String propName)
        {
            if (null != PropertyChanged)
                PropertyChanged.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propName));
        }
        #endregion

        #region IAudioPHY メンバ
        public bool IsRunning {get{return (_audioContext != null);}}
        public int SamplingRate {get{return _samplingRate;}}       
        public int BufferLength {get{return _readBuffer.Length;}}
        public int MinimumBufferFillLength {get{return _minimumBufferLength;}}
        public bool CanWrite {get{return true;}}
        public bool CanRead {get{return true;}}
        public void Start(IAudioSource source)
        {
            if(IsRunning)
                return;

            if(source == null)
                throw new ArgumentNullException("source");            
            _source = source;            

            _audioContext = new AudioContext();                
            AL.Listener(ALListenerf.Gain, 1.0f);            
            _audioSource = AL.GenSource();                
            _audioCapture = new AudioCapture(String.Empty, _samplingRate, OpenTK.Audio.OpenAL.ALFormat.Mono16, _readBuffer.Length);            
            _audioCapture.Start();

            _workerShouldRun = true;
            _thread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(worker));
            _thread.Start();
        }
        public void Stop(bool shouldStopImmediate)
        {
            if (!IsRunning)
                return;

            if (null != _thread)
            {
                _workerShouldRun = false;
                _thread.Join();
                _thread = null;
            }

            if (null != _audioCapture)
            {
                _audioCapture.Stop();
                _audioCapture.Dispose();
                _audioCapture = null;
            }

            if (null != _audioContext)
            {
                int r;
                AL.GetSource(_audioSource, ALGetSourcei.BuffersQueued, out r);
                clearBuffer(r);

                AL.DeleteSource(_audioSource);

                _audioContext.Dispose();
                _audioContext = null;
            }
        }
        #endregion

        #region IDisposable メンバ
        public void Dispose()
        {
            Stop(true);            
        }
        #endregion
    }
}
